
/*
 * Copyright (C) 2014 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { CertificateChainCleaner } from './tls/CertificateChainCleaner'
import { buffer, List, util } from '@kit.ArkTS'
import { socket } from '@kit.NetworkKit'
import { regionMatches, toCanonicalHost } from './utils/StringUtil'
import { cryptoFramework } from '@kit.CryptoArchitectureKit'
import { cert } from '@kit.DeviceCertificateKit'
import { kotlinRequire } from './utils/Utils'
import { AssertionError, IllegalArgumentException, SSLPeerUnverifiedException } from './Error/error'

async function sha1Hash(certdata: socket.X509CertRawData): Promise<string>{
  let x509 = await cert.createX509Cert(certdata)
  let key = x509.getItem(cert.CertItemType.CERT_ITEM_TYPE_PUBLIC_KEY) // 获取证书公钥信息
  // key.data转sha1Hash base64编码
  let sha1 = cryptoFramework.createMd('SHA1')
  await sha1.update({data: key.data})
  let result = await sha1.digest()
  return buffer.from(result.data).toString('base64')  // buffer和array.from.map.join相比性能是否有差距
}

async function sha256Hash(certdata: socket.X509CertRawData): Promise<string>{
  let x509 = await cert.createX509Cert(certdata)
  let key = x509.getItem(cert.CertItemType.CERT_ITEM_TYPE_PUBLIC_KEY) // 获取证书公钥信息
  // key.data转sha256Hash base64编码
  let sha256 = cryptoFramework.createMd('SHA256')
  await sha256.update({data: key.data})
  let result = await sha256.digest()
  return buffer.from(result.data).toString('base64')  // buffer和array.from.map.join相比性能是否有差距
}

// 对证书链中每个证书进行sha256hash
// function pin(certificate: Certificate): String {
//   if (!(certificate instanceof  X509Certificate){
//     throw new Error("Certificate pinning requires X509 certificates")
//   }
//   return `sha256/${sha256Hash(certificate)}`
// }

class Pin {
  pattern: string
  pin: string
  hashAlgorithm: string
  hash: string
  base64Util = new util.Base64Helper()

  constructor(pattern: string, pin: string) {
    this.pattern = pattern
    this.pin = pin
    this.init()
  }
  init(){
    if (!((this.pattern.startsWith("*.") && this.pattern.indexOf("*",1) == -1) ||
      (this.pattern.startsWith("**.") && this.pattern.indexOf("*", 2) == -1) ||
      (this.pattern.indexOf("*") == -1))){
      throw new IllegalArgumentException(`Unexpected pattern: ${this.pattern}`)
    }
    try {
      this.pattern = toCanonicalHost(this.pattern)
    }catch (e) {
      throw new IllegalArgumentException(`Unexpected pattern: ${this.pattern}`)
    }

    if (this.pin.startsWith("sha1/")){
      this.hashAlgorithm = "sha1"
      try {
        this.base64Util.decodeSync(this.pin.substring("sha1/".length))
        this.hash = this.pin.substring("sha1/".length)
      }catch (e) {
        throw new IllegalArgumentException(`Invalid pin: ${this.pin}`)
      }
    }else if (this.pin.startsWith("sha256/")){
      this.hashAlgorithm = "sha256"
      try {
        this.base64Util.decodeSync(this.pin.substring("sha256/".length))
        this.hash = this.pin.substring("sha256/".length)
      }catch (e) {
        throw new IllegalArgumentException(`Invalid pin: ${this.pin}`)
      }
    }else {
      throw new IllegalArgumentException(`pins must start with 'sha256/' or 'sha1/': ${this.pin}`)
    }
  }

  matchesHostname(hostname: string): boolean {
    if (this.pattern.startsWith("**.")) {
      let suffixLength = this.pattern.length - 3
      let prefixLength = hostname.length - suffixLength
      return regionMatches(hostname,hostname.length - suffixLength, this.pattern, 3, suffixLength) && (prefixLength == 0 || hostname[prefixLength - 1] == '.')
    }else if (this.pattern.startsWith("*.")){
      // With * there must be a prefix so include the dot in regionMatches().
      let suffixLength = this.pattern.length - 1
      let prefixLength = hostname.length - suffixLength
      return regionMatches(hostname,hostname.length - suffixLength, this.pattern, 1, suffixLength) && hostname.lastIndexOf('.', prefixLength - 1) == -1
    }else{
      return hostname == this.pattern
    }
  }

  async matchesCertificate(certData: socket.X509CertRawData): Promise<boolean> {
    if (this.hashAlgorithm === "sha256"){
      return this.hash === await sha256Hash(certData)
    }else if(this.hashAlgorithm === "sha1"){
      return this.hash === await sha1Hash(certData)
    }else{
      return false
    }
  }

  toString(): String {
    return `${this.hashAlgorithm}/${this.hash}`
  }

  // equals(other: unknown): boolean {
    // if (this === other) return true
    // if (other !is Pin) return false
    //
    // if (pattern != other.pattern) return false
    // if (hashAlgorithm != other.hashAlgorithm) return false
    // if (hash != other.hash) return false
    //
    // return true
  // }

  // hashCode(): number {
    // var result = pattern.hashCode()
    // result = 31 * result + hashAlgorithm.hashCode()
    // result = 31 * result + hash.hashCode()
    // return result
  // }
}


/**
 * Constrains which certificates are trusted. Pinning certificates defends against attacks on
 * certificate authorities. It also prevents connections through man-in-the-middle certificate
 * authorities either known or unknown to the application's user.
 * This class currently pins a certificate's Subject Public Key Info as described on
 * [Adam Langley's Weblog][langley]. Pins are either base64 SHA-256 hashes as in
 * [HTTP Public Key Pinning (HPKP)][rfc_7469] or SHA-1 base64 hashes as in Chromium's
 * [static certificates][static_certificates].
 *
 * ## Setting up Certificate Pinning
 *
 * The easiest way to pin a host is turn on pinning with a broken configuration and read the
 * expected configuration when the connection fails. Be sure to do this on a trusted network, and
 * without man-in-the-middle tools like [Charles][charles] or [Fiddler][fiddler].
 *
 * For example, to pin `https://publicobject.com`, start with a broken configuration:
 *
 * ```
 * String hostname = "publicobject.com";
 * CertificatePinner certificatePinner = new CertificatePinner.Builder()
 *     .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
 *     .build();
 * OkHttpClient client = OkHttpClient.Builder()
 *     .certificatePinner(certificatePinner)
 *     .build();
 *
 * Request request = new Request.Builder()
 *     .url("https://" + hostname)
 *     .build();
 * client.newCall(request).execute();
 * ```
 *
 * As expected, this fails with a certificate pinning exception:
 *
 * ```
 * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
 * Peer certificate chain:
 *     sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
 *     sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
 *     sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
 *     sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
 * Pinned certificates for publicobject.com:
 *     sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
 *   at okhttp3.CertificatePinner.check(CertificatePinner.java)
 *   at okhttp3.Connection.upgradeToTls(Connection.java)
 *   at okhttp3.Connection.connect(Connection.java)
 *   at okhttp3.Connection.connectAndSetOwner(Connection.java)
 * ```
 *
 * Follow up by pasting the public key hashes from the exception into the
 * certificate pinner's configuration:
 *
 * ```
 * CertificatePinner certificatePinner = new CertificatePinner.Builder()
 *     .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
 *     .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
 *     .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
 *     .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
 *     .build();
 * ```
 *
 * ## Domain Patterns
 *
 * Pinning is per-hostname and/or per-wildcard pattern. To pin both `publicobject.com` and
 * `www.publicobject.com` you must configure both hostnames. Or you may use patterns to match
 * sets of related domain names. The following forms are permitted:
 *
 *  * **Full domain name**: you may pin an exact domain name like `www.publicobject.com`. It won't
 *    match additional prefixes (`us-west.www.publicobject.com`) or suffixes (`publicobject.com`).
 *
 *  * **Any number of subdomains**: Use two asterisks to like `**.publicobject.com` to match any
 *    number of prefixes (`us-west.www.publicobject.com`, `www.publicobject.com`) including no
 *    prefix at all (`publicobject.com`). For most applications this is the best way to configure
 *    certificate pinning.
 *
 *  * **Exactly one subdomain**: Use a single asterisk like `*.publicobject.com` to match exactly
 *    one prefix (`www.publicobject.com`, `api.publicobject.com`). Be careful with this approach as
 *    no pinning will be enforced if additional prefixes are present, or if no prefixes are present.
 *
 * Note that any other form is unsupported. You may not use asterisks in any position other than
 * the leftmost label.
 *
 * If multiple patterns match a hostname, any match is sufficient. For example, suppose pin A
 * applies to `*.publicobject.com` and pin B applies to `api.publicobject.com`. Handshakes for
 * `api.publicobject.com` are valid if either A's or B's certificate is in the chain.
 *
 * ## Warning: Certificate Pinning is Dangerous!
 *
 * Pinning certificates limits your server team's abilities to update their TLS certificates. By
 * pinning certificates, you take on additional operational complexity and limit your ability to
 * migrate between certificate authorities. Do not use certificate pinning without the blessing of
 * your server's TLS administrator!
 *
 * ### Note about self-signed certificates
 *
 * [CertificatePinner] can not be used to pin self-signed certificate if such certificate is not
 * accepted by [javax.net.ssl.TrustManager].
 *
 * See also [OWASP: Certificate and Public Key Pinning][owasp].
 *
 * [charles]: http://charlesproxy.com
 * [fiddler]: http://fiddlertool.com
 * [langley]: http://goo.gl/AIx3e5
 * [owasp]: https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
 * [rfc_7469]: http://tools.ietf.org/html/rfc7469
 * [static_certificates]: http://goo.gl/XDh6je
 */
// @Suppress("NAME_SHADOWING")
export class CertificatePinner {

  pins: Set<Pin>
  certificateChainCleaner?: CertificateChainCleaner = null

  static sha256Hash(data: Uint8Array): Uint8Array{
    let md = cryptoFramework.createMd('SHA256')
    md.updateSync({data: data})
    let result = md.digestSync()
    return result.data
  }

  static sha1Hash(data: Uint8Array): Uint8Array{
    let md = cryptoFramework.createMd('SHA1')
    md.updateSync({data: data})
    let result = md.digestSync()
    return result.data
  }

  static base64(str: Uint8Array): string{
    let buf = buffer.from(str)
    return buf.toString('base64')
  }

  static async pin(certificate: cert.X509Cert) {
    kotlinRequire(!!certificate, "Certificate pinning requires X509 certificates")
    let key = certificate.getPublicKey()
    let pem = key.getEncodedPem(key.format)
    return `sha256/${this.base64(this.sha256Hash(new Uint8Array(buffer.from(pem).buffer)))}`
  }
  constructor(pins: Set<Pin>, certificateChainCleaner?: CertificateChainCleaner) {
    this.pins = pins
    this.certificateChainCleaner = certificateChainCleaner
  }
  /**
   * Confirms that at least one of the certificates pinned for `hostname` is in `peerCertificates`.
   * Does nothing if there are no certificates pinned for `hostname`. OkHttp calls this after a
   * successful TLS handshake, but before the connection is used.
   *
   * @throws SSLPeerUnverifiedException if `peerCertificates` don't match the certificates pinned
   *     for `hostname`.
   */
  // @Throws(SSLPeerUnverifiedException::class)
  async check(hostname: string, certData: socket.X509CertRawData) {
    if (this.certificateChainCleaner !== null){
      // let result = this.certificateChainCleaner.clean(certData,hostname)
      return await this._check(hostname,certData)
    }
    return await this._check(hostname,certData)
  }

  private async _check(hostname: string, certData: socket.X509CertRawData) {
    let pins = this.findMatchingPins(hostname)
    if (pins.isEmpty()) return ""
    // let peerCertificates = cleanedPeerCertificatesFn()
    let sha1: string | null = null
    let sha256: string | null = null
    for (const pin of this.pins) {
      if (pin.hashAlgorithm === "sha256"){
        if (sha256 === null || sha256 === undefined) sha256 = await sha256Hash(certData)
        if (pin.hash === sha256) return ""
      }else if (pin.hashAlgorithm === "sha1"){
        if (sha1 === null || sha1 === undefined) sha1 = await sha1Hash(certData)
        if (pin.hash === sha1) return ""
      }else{
        throw new AssertionError(`unsupported hashAlgorithm: ${pin.hashAlgorithm}`)
      }
    }

    let message = ""
    message += "Certificate pinning failure!"
    message += "\n  Peer certificate chain:"
    // 对服务器证书链进行解析/暂时无法获取证书链/
    message += "null"
    // for (element in peerCertificates) {
    //   append("\n    ")
    //   append(pin(element))
    //   append(": ")
    //   append(element.subjectDN.name)
    // }
    message += "\n  Pinned certificates for "
    message += hostname
    message += ":"
    for (const pin of this.pins) {
      message += "\n    "
      message += pin.toString()
    }
    throw new SSLPeerUnverifiedException(message)
  }

  /**
   * Returns list of matching certificates' pins for the hostname. Returns an empty list if the
   * hostname does not have pinned certificates.
   */
  findMatchingPins(hostname: string): List<Pin> {
    let pins: List<Pin> = new List<Pin>()
    for(const pin of this.pins){
      if (pin.matchesHostname(hostname)){
        pins.add(pin)
      }
    }
    return pins
  }

  /** Returns a certificate pinner that uses `certificateChainCleaner`. */
  withCertificateChainCleaner(certificateChainCleaner: CertificateChainCleaner): CertificatePinner {
    if (this.certificateChainCleaner === certificateChainCleaner){
      return this
    }
    return new CertificatePinner(this.pins, certificateChainCleaner)
  }

  equals(other: unknown): boolean {
    if (!(other instanceof CertificatePinner)) return false
    if (other.certificateChainCleaner !== this.certificateChainCleaner) return false
    // 比较 Set 的内容
    return this.pinsEqual(this.pins, other.pins)
  }

  private pinsEqual(a: Set<Pin>, b: Set<Pin>): boolean {
    if (a.size !== b.size) return false
    // 将 Set 转换为数组并比较
    const aArray = Array.from(a)
    const bArray = Array.from(b)
    for (const pinA of aArray) {
      let found = false
      for (const pinB of bArray) {
        if (pinA.pattern === pinB.pattern &&
            pinA.hashAlgorithm === pinB.hashAlgorithm &&
            pinA.hash === pinB.hash) {
          found = true
          break
        }
      }
      if (!found) return false
    }
    return true
  }

  hashCode(): number {
    let result = 37
    for (const pin of this.pins) {
      result = 41 * result + this.hashString(pin.pattern)
      result = 41 * result + this.hashString(pin.hashAlgorithm)
      result = 41 * result + this.hashString(pin.hash)
    }
    if (this.certificateChainCleaner) {
      result = 41 * result + 1
    }
    return result
  }

  private hashString(str: string): number {
    let hash = 0
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i)
      hash = ((hash << 5) - hash) + char
      hash = hash & hash
    }
    return hash
  }


  static get Builder(){
    class Builder {
      pins = new List<Pin>()

      add(pattern: string, pins: string[]): Builder {
        for (const pin of pins){
          this.pins.add(new Pin(pattern, pin))
        }
        return this;
      }

      build(): CertificatePinner {
        let set = new Set<Pin>()
        for (const pin of this.pins){
          set.add(pin)
        }
        return new CertificatePinner(set)
      }
    }
    return Builder
  }
}

export const DEFAULT = new CertificatePinner.Builder().build()
